/**
 * 等高列表界面组件
 * 具备的能力：触底、触顶自动索取数据，使用确定数量的Dom元素渲染数据；
 * 限制条件：列表项高度必须相同且不能变化
 * Author 李波 晓伦
 */
export class EqualHeightList {
  /**
   * 使用者传入的参数
   */
  private param!: TEqualHeightListParam
  /**
   * 总高度=总行数*行高
   */
  private totalHeight!: number
  /**
   * 一页数据显示的行数，默认值33，这个数字会随着显示区域高度的变化而变化
   */
  private pageSize: number = 33
  /**
   * 一行数据的默认高度
   */
  private itemHeight: number = 62
  /**
   * 容器大小变化监视器
   */
  private resizeObserver!: ResizeObserver

  /**
   * 滚动事件
   * 无论是鼠标滚轮滚动，还是拖动滚动条滚动，最终都会触发此事件
   * 滚动条滚动的过程中会不断的触发索取数据的事件
   * 此方法使用marginTop样式控制第一行数据只显示一部分的情况
   * 使用者不能在数据项DOM元素上使用marginTop样式，一般情况下也不会有人这么干
   */
  private async scroll() {
    let container = this.getContainer()
    if (container.children.length < 1) return
    let scrollerBox = container.lastChild as HTMLElement
    let startIndex = 0
    let firstItemHiddenSpan = 0
    if (Math.ceil(scrollerBox.scrollTop + scrollerBox.clientHeight) >= scrollerBox.scrollHeight) {
      //滚动条触底
      startIndex = this.param.totalRowCount - this.pageSize
      firstItemHiddenSpan = container.clientHeight - this.pageSize * this.itemHeight
    } else if (scrollerBox.scrollTop != 0) {
      startIndex = Math.floor(scrollerBox.scrollTop / this.itemHeight) //向下取整，首行可能显示不全
      firstItemHiddenSpan = startIndex * this.itemHeight - scrollerBox.scrollTop
    }
    await this.param.onDataRequired({
      startIndex,
      pageSize: this.pageSize,
    })
    for (let i = 0; i < container.children.length - 1; i++) {
      let node = container.children[i] as HTMLElement
      if (i === 0) {
        node.style.marginTop = firstItemHiddenSpan + 'px'
      } else {
        node.style.removeProperty('margin-top')
      }
    }
  }
  /**
   * 鼠标滚轮滚动事件
   * 在此事件中调整了模拟滚动条的位置
   */
  private mousewheel(e: WheelEvent | any) {
    let container = this.getContainer()
    let scrollerBoxDom = container.lastChild as HTMLElement
    let curTop = scrollerBoxDom.scrollTop
    let maxTop = this.totalHeight - container.clientHeight
    curTop += e.deltaY
    if (curTop < 0) curTop = 0
    else if (curTop > maxTop) {
      curTop = maxTop
    }
    scrollerBoxDom.scrollTop = curTop
    return false
  }

  /**
   * 鼠标移入数据区域事件
   * 判断是否需要显示滚动条，
   * 如果可视区域大于数据总高度，则不显示；
   * 否则显示，120毫秒动画
   */
  private mouseenter(e: MouseEvent) {
    let scrollerBoxDom = (<HTMLElement>e.target).lastChild as HTMLElement
    if (scrollerBoxDom.clientHeight > scrollerBoxDom.children[0].clientHeight) return
    scrollerBoxDom.animate([{ opacity: 0 }, { opacity: 1 }], {
      duration: 120,
      fill: 'forwards',
    })
  }

  /**
   * 鼠标移出数据区域事件
   * 如果滚动条处于显示状态，则隐藏滚动条，680毫秒动画
   */
  private mouseleave(e: MouseEvent) {
    let scrollerBoxDom = (<HTMLElement>e.target).lastChild as HTMLElement
    if (scrollerBoxDom.clientHeight > scrollerBoxDom.children[0].clientHeight) return
    scrollerBoxDom.animate([{ opacity: 1 }, { opacity: 0 }], {
      duration: 680,
      fill: 'forwards',
    })
  }
  /**
   * 数据容器大小变化事件
   * 容器高度变化时，重新计算pageSize，重新索取数据
   */
  private async updateContainerSize() {
    const _pageSize = Math.ceil(this.getContainer().clientHeight / this.itemHeight) //向上取整，多取一行
    if (this.pageSize === _pageSize) return
    this.pageSize = _pageSize
    await this.scroll()
  }
  /**
   * 更新数据总量变
   * 调用此方法会触发数据索取事件
   */
  async updateToatalRowCount(totalRowCount: number) {
    this.param.totalRowCount = totalRowCount
    this.totalHeight = this.itemHeight * this.param.totalRowCount
    let scroller = this.getContainer().lastChild?.lastChild as HTMLElement
    scroller.style.height = this.totalHeight + 'px'
    await this.scroll()
  }
  /**
   * 获取数据容器的Dom元素
   */
  private getContainer() {
    return document.querySelector(this.param.containerSelector) as HTMLElement
  }

  /**
   * 创建一个div元素，并把指定的样式附加到这个div元素上，这是个工具方法
   * @param className
   * @returns
   */
  private createDomWithClass(container: HTMLElement, className: string) {
    let dom = document.createElement('div')
    dom.classList.add(className)
    container.append(dom)
    return dom
  }
  /**
   * 注册数据区域的mousewheel、mouseenter、mouseleave事件
   * 也注册了滚动条容器的scroll事件
   * 也注册了数据区域大小变化的事件
   */
  private eventListener(container: HTMLElement) {
    container.addEventListener('mouseenter', this.mouseenter.bind(this))
    container.addEventListener('mouseleave', this.mouseleave.bind(this))
    container.addEventListener('mousewheel', this.mousewheel.bind(this))
    let scrollerBox = container.lastChild as HTMLElement
    scrollerBox.addEventListener('scroll', this.scroll.bind(this))
    this.resizeObserver = new ResizeObserver(() => this.updateContainerSize())
    this.resizeObserver.observe(container)
  }
  /**
   * 创建滚动条容器Dom元素
   * 其内置一个子DOM元素，该占位元素用于撑破滚动条容器的Dom元素，使其显示滚动条
   */
  private createScrollerBox(container: HTMLElement) {
    let scrollerBox = this.createDomWithClass(container, 'equalHeightScrollerBox')
    let scroller = this.createDomWithClass(scrollerBox, 'equalHeightScroller')
    if (!container.children[0]) return
    this.itemHeight = container.children[0].getBoundingClientRect().height
    this.totalHeight = this.itemHeight * this.param.totalRowCount
    scroller.style.height = this.totalHeight + 'px'
  }
  /**
   * 销毁对象
   * 注销对象上的事件
   */
  destroy() {
    let container = this.getContainer()
    this.resizeObserver.unobserve(container)
    this.resizeObserver.disconnect()
    container.removeEventListener('mouseenter', this.mouseenter)
    container.removeEventListener('mouseleave', this.mouseleave)
    container.removeEventListener('mousewheel', this.mousewheel)
    let scrollerBox = container.lastChild as HTMLElement
    scrollerBox.removeEventListener('scroll', this.scroll)
  }
  /**
   * 初始化等高列表组件
   * @param param 初始化参数
   */
  init(param: TEqualHeightListParam) {
    this.param = param
    let container = this.getContainer()
    container.classList.add('equalHeightContainer')
    this.createScrollerBox(container)
    this.eventListener(container)
  }
}
export interface TEqualHeightListParam {
  /**
   * 挂载等高列表的容器选择器
   */
  containerSelector: string
  /**
   * 更新视图数据的回调函数
   * startIndex为视图的起始行下标
   * pageSize为视图一页数据行数
   * 对应的SQL语句为：SELECT * FROM COMPANY LIMIT [pageSize] OFFSET [startIndex];
   */
  onDataRequired: (o: { startIndex: number; pageSize: number }) => any
  /**
   * 总数据行数
   */
  totalRowCount: number
}
